import sys
import time
import pickle
import signal
import psutil
import multiprocessing as mp

from cc.appcoin2.strategy.py_strat.util.order_manager import OrderManager
from cc.appcoin2.support.py_module.py_driver import (
    PyDriver,
    PyLogger,
    PyExecSpecPickleable,
    ExecutionType)


class GracefulKiller:
  def __init__(self, parent_pid):
    signal.signal(signal.SIGTERM, self.exit_gracefully)
    signal.signal(signal.SIGINT, self.exit_gracefully)
    self._parent_pid = parent_pid

  def exit_gracefully(self, *args):
    if not self._parent_pid:
      return
    parent = psutil.Process(self._parent_pid)
    for child in parent.children(recursive=True):
      print("kill {}".format(child.pid))
      child.kill()
    print("kill {}".format(parent.pid))
    parent.kill()


class StrategyBase:
  def __init__(self, driver_config):
    self._driver_config = driver_config
    self._driver = None

    self._cpp_log_file_path = None
    self._redirect_py_print = False
    self._py_log_file_path = None

    self._og_name_list = []

    self._order_manager = OrderManager()

    self._strat_pipe_outgoing = None
    self._strat_pipe_incoming = None
    self._drive_pipe_outgoing = None
    self._drive_pipe_incoming = None

    self._sub_process = None

    self._enable_telemetry = False

  def set_pipe(self, pipe0, pipe1):
    self._strat_pipe_outgoing = pipe0[0]
    self._strat_pipe_incoming = pipe1[0]
    self._drive_pipe_outgoing = pipe1[1]
    self._drive_pipe_incoming = pipe0[1]

  def redirect_cpp_log_to_file(self, log_path, redirect_py_print: bool):
    if not log_path:
     return
    # TODO: check file path
    self._cpp_log_file_path = log_path
    self._redirect_py_print = redirect_py_print

  def redirect_py_log_to_file(self, log_path, redirect_py_print: bool):
    if not log_path:
     return
    # TODO: check file path
    self._py_log_file_path = log_path
    self._redirect_py_print = redirect_py_print

  def get_all_working_orders(self, me='ALL'):
    return self._order_manager.get_all_working_orders(me)

  # outgoing
  def submit_order(self, exec_spec: dict):
    if 'og_name' not in exec_spec:
      return
    if 'order_spec' not in exec_spec:
      return
    og_name = exec_spec['og_name']
    if og_name not in self._og_name_list:
      return
    py_exec_spec = PyExecSpecPickleable(og_name)
    py_exec_spec.set_exec_type(ExecutionType.Submit)
    py_exec_spec.set_order_spec(exec_spec['order_spec'])
    msg = pickle.dumps(py_exec_spec)
    self._strat_pipe_outgoing.send_bytes(msg)

  def cancel_order(self, exec_spec: dict):
    if 'og_name' not in exec_spec:
      return
    if 'proc_order_id' not in exec_spec:
      return
    og_name = exec_spec['og_name']
    if og_name not in self._og_name_list:
      return
    py_exec_spec = PyExecSpecPickleable(og_name)
    py_exec_spec.set_exec_type(ExecutionType.Cancel)
    py_exec_spec.set_proc_order_id(exec_spec['proc_order_id'])
    msg = pickle.dumps(py_exec_spec)
    self._strat_pipe_outgoing.send_bytes(msg)

  # incoming
  def handle_incoming_msg(self, msg):
    obj = pickle.loads(msg)
    if not hasattr(obj, 'type'):
      return
    #print("incoming: {}".format(obj.as_dict()))
    if obj.type == 'ACCOUNT_INFO_UPDATE_TYPE':
      if not hasattr(self, "on_account_info"):
        return
      self.on_account_info(obj)
    elif obj.type == 'PRODUCT_INFO_UPDATE_TYPE':
      self._order_manager.on_order_update(obj)  # update local order manager
      if not hasattr(self, "on_account_order"):
        return
      self.on_account_order(obj)
    elif obj.type == 'BOOK_FEED':
      if not hasattr(self, "on_book_feed"):
        return
      self.on_book_feed(obj)
    elif obj.type == 'TRADE_FEED':
      if not hasattr(self, "on_trade_feed"):
        return
      self.on_trade_feed(obj)
    elif obj.type == 'ORDER_SNAPSHOT':
      self._order_manager.on_order_snapshot(obj) # update local order manager
    else:
      print("error: unsupported type: {}".format(obj.as_dict))

  # run in strategy thread
  def run_strat(self):
    print("parent process: {}".format(mp.current_process().pid))
    # redirect log
    if self._py_log_file_path:
      print("parent process: redirect log to file: {}".format(self._py_log_file_path))
      PyDriver.redirect_cpp_log(self._py_log_file_path, 2)
      sys.stdout = PyLogger()
    # start strategy
    try:
      # loop
      while True:
        if not self._strat_pipe_incoming.poll():
          if not self._sub_process.is_alive():  # TODO(daniel): any better solution?
            raise "exit due to driver process dead"
          continue
        msg = self._strat_pipe_incoming.recv_bytes()
        if msg is None:
          continue
        self.handle_incoming_msg(msg)
    except Exception as exc:
      import traceback
      print(traceback.format_exc())
      while self._sub_process.is_alive():
        self._sub_process.terminate()
        time.sleep(1)
      raise

  # run in cpp driver thread
  def run_async(self):
    print("child  process: {}".format(mp.current_process().pid))
    # redirect log
    if self._cpp_log_file_path:
      print("child  process: redirect log to file: {}".format(self._cpp_log_file_path))
      PyDriver.redirect_cpp_log(self._cpp_log_file_path, 2)
      sys.stdout = PyLogger()
    # start driver
    self._driver = PyDriver(self._driver_config,
                            self._drive_pipe_incoming,
                            self._drive_pipe_outgoing,
                            self._enable_telemetry)
    self._driver.run()

  def run(self):
    process_killer = GracefulKiller(mp.current_process().pid)
    pipe1 = mp.Pipe()
    pipe2 = mp.Pipe()
    self.set_pipe(pipe1, pipe2)

    def run_drive(strategy):
      strategy.run_async()

    self._sub_process = mp.Process(target=run_drive, args=(self,))
    self._sub_process.start()

    self.run_strat()
